package com.hhf.job.config;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.hhf.job.entity.BaseJob;
import com.hhf.job.entity.BaseJobLog;
import com.hhf.job.service.BaseJobLogService;
import com.hhf.job.service.BaseJobService;
import com.hhf.job.util.SpringUtil;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.client.RestTemplate;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;

@Slf4j
@Setter
@Configuration
@ConfigurationProperties(prefix = "job.cluster")
@EnableScheduling
public class BaseJobConfig implements SchedulingConfigurer {

    private List<String> serviceUrl;
    private boolean enable;
    @Resource
    private RestTemplate restTemplate;
    @Autowired
    private BaseJobService baseJobService;
    @Autowired
    private BaseJobLogService baseJobLogService;

    private volatile ScheduledTaskRegistrar taskRegistrar;

    private final ConcurrentHashMap<Integer, ScheduledFuture<?>> scheduledFutures = new ConcurrentHashMap<>();

    private final ConcurrentHashMap<Integer, BaseJob> tasks = new ConcurrentHashMap<>();

    private static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {

        taskRegistrar.setScheduler(taskScheduler());
        this.taskRegistrar = taskRegistrar;
        refresh();
    }

    public void refreshAll() {
        if(enable) {
            if(!CollectionUtils.isEmpty(serviceUrl)) {
                for (String url : serviceUrl) {
                    restTemplate.getForObject(url, String.class);
                    try {
                        // 防止集中刷新，访问数据库锁表
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        log.error(e.getMessage(), e);
                    }
                    log.info("节点:{}刷新配置",url);
                }
            }
        } else {
            refresh();
        }
    }

    public void refresh() {
        List<BaseJob> jobs = baseJobService.getJob();
        Set<Integer> ids = scheduledFutures.keySet();
        for (Integer id : ids) {
            if (!isExist(id, jobs)) {
                scheduledFutures.get(id).cancel(false);
                scheduledFutures.remove(id);
            }
        }

        for (BaseJob job: jobs) {
            if(StringUtils.isBlank(job.getCron())) {
                continue;
            }
            Integer jobId = job.getJobId();
            if(scheduledFutures.containsKey(jobId) && tasks.get(jobId).equals(job)) {
                continue;
            }
            if (scheduledFutures.containsKey(jobId)) {
                scheduledFutures.get(jobId).cancel(false);
                scheduledFutures.remove(jobId);
                tasks.remove(jobId);
            }
            ScheduledFuture<?> schedule = taskRegistrar.getScheduler().schedule(getRunnable(job), getTrigger(job));
            scheduledFutures.put(jobId,schedule);
            tasks.put(jobId,job);
        }
    }

    @Bean
    public Executor taskScheduler() {
        //设置线程名称
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
        return scheduledExecutorService;
    }

    public Object execTask(BaseJob job) throws ClassNotFoundException {
        String params = job.getParams();
        String[] paramArray = null;
        Class<?>[] paramTypeArray = new Class[0];
        if (StringUtils.isNotBlank(params)) {
            paramArray = params.split("\\s*,|，\\s*");
            int length = paramArray.length;
            paramTypeArray = new Class[length];
            for (int i = 0; i< length; i++) {
                paramTypeArray[i]=String.class;
            }
        }
        Class<?> clazz = Class.forName(job.getClassName());
        String className = lowerFirstCapse(clazz.getSimpleName());
        Object bean = SpringUtil.getBean(className);
        Method method = ReflectionUtils.findMethod(bean.getClass(), job.getMethod(),paramTypeArray);
        Assert.notNull(method,bean.getClass()+"#"+job.getMethod()+" not exists");
        Object resultObj = ReflectionUtils.invokeMethod(method, bean, paramArray);
        return resultObj;
    }

    private Runnable getRunnable(BaseJob baseJob) {

        return new Runnable() {
            @Override
            public void run() {
                // 实时获取job执行状态字段，根据执行状态字段值和InnoDb引擎行级排它锁update的执行结果来判断集群环境下其它节点是否需要执行
                Integer jobId = baseJob.getJobId();
                BaseJob job = baseJobService.getJobById(jobId);
                if(job.getExecStatus() == 1) {
                    log.debug("任务已经在执行中，可能已被其它节点执行");
                    return;
                }
                boolean b = baseJobService.updateJobExecStatus(jobId, 0, 1);
                if(!b) {
                    log.debug("任务已有节点执行，本节点不执行");
                    return;
                }
                log.info("**********************任务{}开始在本节点执行********************",jobId);
                BaseJobLog jobLog = new BaseJobLog();
                BeanUtils.copyProperties(job,jobLog);
                jobLog.setExecType(1);
                jobLog.setExecStatus(1);
                jobLog.setCreated(LocalDateTime.now());
                baseJobLogService.save(jobLog);
                try {
                    execTask(job);
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                    jobLog.setExecStatus(0);
                    jobLog.setExecMsg(pringStackTraceToString(e));
                } finally {
                    jobLog.setUpdated(LocalDateTime.now());
                    baseJobLogService.saveOrUpdate(jobLog);
                    baseJobService.updateJobExecStatus(job.getJobId(), 1, 0);
                }
            }
        };
    }

    private Trigger getTrigger(BaseJob job) {
        return new Trigger() {
            @Override
            public Date nextExecutionTime(TriggerContext triggerContext) {
                CronTrigger trigger = new CronTrigger(job.getCron());
                Date nextExec = trigger.nextExecutionTime(triggerContext);
                baseJobService.updateNextExecTime(job.getJobId(), nextExec);
                log.debug("任务【{}(任务ID：{})】下一次执行时间:{}",job.getJobName(),job.getJobId(),sdf.format(nextExec));
                return nextExec;
            }
        };
    }

    /**
     * 转换首字母小写
     *
     * @param str
     * @return
     */
    private static String lowerFirstCapse(String str) {
        char[] chars = str.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

    private boolean isExist(Integer id, List<BaseJob> jobs) {
        for(BaseJob job: jobs) {
            if(id.equals(job.getJobId())) {
                return true;
            }
        }
        return false;
    }

    private static String pringStackTraceToString(Throwable t) {
        StringWriter sw = new StringWriter();
        t.printStackTrace( new PrintWriter(sw, true));
        return sw.getBuffer().toString();
    }

    @PreDestroy
    public void destory() {
        taskRegistrar.destroy();
    }

    /**
     * 启动时所有任务状态置为未执行
     */
    @PostConstruct
    public void init() {
        baseJobService.updateJobExecStatus(null,null,0);
    }
}
